iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 10
0
自我挑戰組

Android Architecture 及 Unit Test系列 第 10

[Day 10] Data layer with Room and repository pattern

  • 分享至 

  • xImage
  •  

今天要開始規劃我們的資料層,上週有提到 data layer 裡暴露及提供給其他層次的是 Repository (可參考第二天的圖),而 Repository 的資料則是由 Local/Remote DataSource 提供; 在本地端的 DataSource 則是由 Room 負責,下面我們來實現這個架構。

初始化 Room

關於 Room 的介紹及原理等網路上都已經有很多很多教學,因此我會省略一些內容,想要了解詳細內容可以到官網學習。

首先是 gradle :

dependencies {
    def roomVersion = "2.1.0"
    implementation "androidx.room:room-runtime:$roomVersion"
    kapt "androidx.room:room-compiler:$roomVersion"
    implementation "androidx.room:room-ktx:$roomVersion"
}

接著建立一個用來表示工作事項資料的 Entity --- Task ,這會是接下來在 Room 裡使用的 Table :

@Entity(tableName = "Tasks")
data class Task (

    @ColumnInfo(name = "title")
    var title: String = "",

    @ColumnInfo(name = "description")
    var description: String = "",

    @ColumnInfo(name = "completed")
    var isCompleted: Boolean = false,

    @PrimaryKey
    @ColumnInfo(name = "entryid")
    var id: String = UUID.randomUUID().toString()
) {
    val titleForList: String
        get() = if (title.isNotEmpty()) title else description

    val isActive
        get() = !isCompleted

    val isEmpty
        get() = title.isEmpty() || description.isEmpty()
}

這邊我們把 id 當作是 PrimaryKey ,其內容是一個 random 的 UUID。

再來實作定義 SQL 語句的 TasksDao

@Dao
interface TasksDao {

    @Query("SELECT * FROM tasks")
    suspend fun getTasks(): List<Task>

    @Query("SELECT * FROM tasks WHERE entryid = :taskId")
    suspend fun getTaskById(taskId: String): Task?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertTask(task: Task): Long

    @Update
    suspend fun updateTask(task: Task): Int

    @Query("UPDATE Tasks SET completed = :completed WHERE entryid = :taskId")
    suspend fun updateComplete(taskId: String, completed: Boolean)

    @Query("DELETE FROM Tasks WHERE entryid = :taskId")
    suspend fun deleteTaskById(taskId: String): Int

    @Query("DELETE FROM Tasks")
    suspend fun deleteTasks()

    @Query("DELETE FROM Tasks WHERE completed = 1")
    suspend fun deleteCompletedTasks(): Int
}

值得一提的是現在 Room 已經支持使用 Kotlin Coroutines ,所以我的 TasksDao 已經改成了 suspend function 。

接著建立資料庫 ToDoRoomDatabase

@Database(entities = [Task::class], version = 1, exportSchema = false)
abstract class ToDoRoomDatabase : RoomDatabase() {
    abstract fun taskDao(): TasksDao
}

一樣 Room 需要繼承 RoomDatabase ,這邊我們讓 DB 放入 Table Task ,一般來說開發時不太需要輸出 DB 的 schema ,除非有特殊用途例如 Database 的 Migration Unit Test

我們再寫一個可以獲取 Database 的 class ,這個 class 可以提供 Database 的實體:

object ServiceInjector {

    @Volatile
    private var database: ToDoDatabase? = null
    
    fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

如此可以這樣獲取資料:

val dao = ServiceInjector.createDateBase(context).tasksDao()
val tasks: List<Task> = dao.getTasks()
......

現在已經完成 Room 的初始化及 Dao 的使用,再來我們要處理架構的部分。

建立 DataSource

DataSource 顧名思義就是指資料的來源,我們會在這個層次將各種不同資料依據來源的不同而拆分。

其實嚴格上來說,這個專案因為比較簡單的緣故,是可以不需要拆出這一個層次,但是我們為了要模仿一般專案的情境,這裡就把 Task 資料的來源假設成有 Remote(Network) 及 Local(DataBase) 兩種,當然如果有多個資料來源可以再繼續拆分,這就依實際情況來決定。

首先我們使用一個 interface 來定義使用 Task 資料的方法:

interface TasksDataSource {

    suspend fun getTasks(): Result<List<Task>>

    suspend fun getTask(taskId: String): Result<Task>

    suspend fun saveTask(task: Task)

    suspend fun completeTask(task: Task)

    suspend fun completeTask(taskId: String)

    suspend fun activateTask(task: Task)

    suspend fun activateTask(taskId: String)

    suspend fun cleanCompleteTasks()

    suspend fun deleteAllTasks()

    suspend fun deleteTask(taskId: String)
}

接下來各種不同的資料來源再各自實作 interface ,就可以把資料來源拆開來。這邊其實沒有硬性規定必須使用同一個 interface ,只要把握住 "依據不同資料來源劃分" 這個思路即可。

由於程式碼稍長,這裡提供一個範例給大家餐考。

使用 Repository Pattern

這是一個很常見的設計方式,簡單來說 Repository 大約是對應 MVC 裡面的 Model 層,負責管理資料的進出, Domain Layer 只管對 Data Layer 的 Repository 請求或是存取資料,不需要在乎 Repository 的資料來源是誰,實際要使用哪個資料來源由 Repository 自己決定。

我們會在 Repository 這個層次使用 Remote Data 及 Local Data ,具體要用哪個再根據實際狀況決定。首先我們先透過 interface 來定義 Repository 的方法:

interface ITasksRepository {

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>

    suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>

    suspend fun saveTask(task: Task)

    suspend fun completeTask(task: Task)

    suspend fun completeTask(taskId: String)

    suspend fun activateTask(task: Task)

    suspend fun activateTask(taskId: String)

    suspend fun clearCompletedTasks()

    suspend fun deleteAllTasks()

    suspend fun deleteTask(taskId: String)
}

由於這邊程式碼有點長,如果有興趣可以從這裡點擊進入查看詳細的作法。

最後在 ServiceInjector 補上獲得 DataSource 及 Repository 的方法即可。

object ServiceInjector {

    ......
    fun createTasksRepository(context: Context): TasksRepository {
        return DefaultTasksRepository(
            FakeTasksRemoteDataSource, 
            createTaskLocalDataSource(context)
        )
    }

    fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }
}

這樣就大致完成 Data Layer 的程式了。


上一篇
[Day 9] Navigation Component:Part 2
下一篇
[Day 11] Travis CI
系列文
Android Architecture 及 Unit Test30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言